package com.espirit.eap.util.tagAdaptor;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.Resource;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Service;

import com.espirit.eap.manager.I18nService;
import com.espirit.eap.pagelayout.AvabButton;
import com.espirit.eap.pagelayout.BaseTypeModel;
import com.espirit.eap.pagelayout.BlockSection;
import com.espirit.eap.pagelayout.Element;
import com.espirit.eap.pagelayout.ElementService;
import com.espirit.eap.pagelayout.ElementService.ElementCallback;
import com.espirit.eap.pagelayout.FlatSection;
import com.espirit.eap.pagelayout.IElement;
import com.espirit.eap.pagelayout.Layout;
import com.espirit.eap.pagelayout.Model;
import com.espirit.eap.pagelayout.ModelField;
import com.espirit.eap.pagelayout.ModelService;
import com.espirit.eap.pagelayout.PageCache;
import com.espirit.eap.pagelayout.PageReference;
import com.espirit.eap.pagelayout.SectionElement;
import com.espirit.eap.util.TaglibManager;
import com.googlecode.cswish.Config;
import com.googlecode.cswish.model.CssStyle;
import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.model.PropertyInfo;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.ModelPropertyInfo;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.OgnlExUtil;

@Service
public class TagAdaptorUtil {
	
	private static final Log logger = LogFactory.getLog(TagAdaptorUtil.class);
	
	@Resource
	private BeanIntrospector beanIntrospector;
	
	@Resource
	private OgnlExUtil ognlExUtil;
	
	@Resource
	private I18nService i18nService;
	
	@Resource
	private TaglibManager tagManager;
	
	@Resource
	private ElementService elementService;
	
	@Resource
	private ModelService modelService;
	
	@Resource
	private Config config;
	
	private int nameIndex;
	
	/**
	 * 1. update the label to i18n
	 * 2. update the pageElement.label if the i18n key is changed
	 * 
	 * @param properties
	 * @param element
	 * @param pageCache
	 */
	public boolean updateLabel(Map<String, String> properties, Element element, PageCache pageCache) {
		String oldLabel = element.getI18n();
		String label = properties.get("label");
		
		boolean changed;
		if ((label == null || label.length() == 0)) {
			if (element.getI18n() != null) {
				element.setI18n(null);
				changed = true;
			} else {
				changed = false;
			}
		} else {
			element.setI18n(label);
			
			changed = !StringUtils.equals(label, oldLabel);
		}
		
		properties.remove("label");
		return changed;
	}
	
	/**
	 * Update properties to sectionElement, priority:<br>
	 * sectionElement.property --> sectionElement.exProperty
	 * 
	 * @param properties
	 * @param defaultProperties
	 * @param sectionElement
	 */
	public void updateElement(Map<String, String> properties, Map<String, Object> defaultProperties, SectionElement sectionElement) {
		ModelInfo sectionElementModelInfo = beanIntrospector.getModelInfo(SectionElement.class.getName());
		
		Map<String, String> exProperties = sectionElement.getExProperties();
		if (exProperties == null) {
			exProperties = new HashMap<String, String>();
		}
		for (Entry<String, String> entry : properties.entrySet()) {
			String property = entry.getKey();
			boolean isTagId = false;
			if ("textModel".equals(property)) {		// textModel is mapped to readOnly 
				property = "readOnly";
			} else if ("id".equals(property)) {
				 isTagId = true;
			}
			
			PropertyInfo propertyInfo =  isTagId ? null : sectionElementModelInfo.getProperty(property);
			if (propertyInfo != null) {
				setPropertyValue(sectionElement, property, entry.getValue(), sectionElementModelInfo, propertyInfo);
			} else {
				String value = entry.getValue();
				if (value != null && value.length() > 0 && isDifferent(value, defaultProperties.get(entry.getKey()))) {
					exProperties.put(entry.getKey(), entry.getValue());
				} else {
					exProperties.remove(entry.getKey());
				}
			}
		}
		
		if (exProperties.size() > 0) {
			sectionElement.setExProperty(exProperties);
		} else {
			sectionElement.setExProperty((String)null);
		}
	}
	
	private boolean isDifferent(String value, Object defaultValue) {
		return defaultValue == null || !value.equals(defaultValue.toString());
	}

	public void updateElement(Map<String, String> properties, Map<String, Object> defaultProperties, Element element) {
		ModelInfo elementInfo = beanIntrospector.getModelInfo(element.getClass().getName());
		Map<String, String> exProperties = element.getExProperties();
		if (exProperties != null) {
			exProperties.clear();					// clean all the old property
		}
		
		// populate all the property
		for (Entry<String, String> entry : properties.entrySet()) {
			String property = entry.getKey();
			String value = entry.getValue();
			 
			if ("columnCount".equals(property)) {		// columnCount is mapped to colsPerRow
				property = "colsPerRow";
			}
			
			PropertyInfo propertyInfo = elementInfo.getProperty(property);
			if (!"id".equals(property) && propertyInfo != null && propertyInfo.getType().startsWith("java.")
					&& propertyInfo.getWriteMethod() != null) {
				setPropertyValue(element, property, value, elementInfo, propertyInfo);
			} else {
				if (value != null && value.length() > 0 && isDifferent(value, defaultProperties.get(property))) {
					if (exProperties == null) {
						exProperties = new HashMap<String, String>();
					}
					exProperties.put(property, value);
				}
			}
		}
		
		if (exProperties != null) {
			// reset the property to force updating exProperties to exProperty
			element.setExProperty(exProperties);
		} else {
			element.setExProperty("");
		}
	}

	private String getShortName(String name) {
		if (name.startsWith("_gen_")) {
			// exmaple: _gen_xxx_idyyy ==> yyy
			int endIndex = name.lastIndexOf('_');
			if (endIndex > 0) {
				int beginIndex = name.lastIndexOf('_', endIndex - 1);
				if (beginIndex > 0) {
					name = name.substring(beginIndex + 1, endIndex);
				} else {
					name = "";
				}
			} else {
				name = "";
			}
		}
		return name;
	}
	
	private void setPropertyValue(Object model, String property, String value, ModelInfo modelInfo, PropertyInfo propertyInfo) {
		if (value == null || value.length() == 0) {
			propertyInfo.setValue(model, null, modelInfo);
		} else {
			if (propertyInfo.getType().equals(String.class.getName())) {
				propertyInfo.setValue(model, value, modelInfo);
			} else {
				Object val = ognlExUtil.convertValue(value, propertyInfo.getTypeClass(), propertyInfo.getName());
				propertyInfo.setValue(model, val, modelInfo);
			}
		}
	}
	
	/**
	 * Find the matched child, don't search the reference element(SectionElement.element)
	 * 
	 * @param <T>
	 * @param element
	 * @param targetType
	 * @param includeChildType
	 * @return
	 * @see findElement(Element root, String name, Class<T> targetType)
	 */
	public <T> T findElement(Element element, Class<T> targetType, boolean includeChildType) {
		if (includeChildType && targetType.isAssignableFrom(element.getClass())
				|| !includeChildType && targetType.equals(element.getClass())) {
			return (T) element;
		}
		
		List<Element> children = element.getChildren();
		if (children != null) {
			for (Element child : children) {
				T ret = findElement(child, targetType, includeChildType);
				if (ret != null) {
					return ret;
				}
			}
		}
		return null;
	}
	
	public class ParentElementInfo {
		public Element parent;
		public SectionElement refWrapper;			// the element is referenced by the sectionElement
		
		public ParentElementInfo(Element parent) {
			this.parent = parent;
		}
	}

	public void addToParent(BlockSection element, Layout root, IElement parent, Map<String, Object> properties) {
		if (parent == null) {
			parent = root;
		}
		
		Integer positionCol = (Integer) properties.get(TagElementAdaptor.POSITION_COL);
		Integer positionRow = (Integer) properties.get(TagElementAdaptor.POSITION_ROW);
		
		BlockSection blockSection = (BlockSection)element;
		int itemNumber = (positionCol == null ? 0 : positionCol) * 100
				+ (positionRow == null ? 0 : positionRow);
		blockSection.setItemNumber(itemNumber);
		
		if (parent instanceof Layout) {
			Layout layout = (Layout) parent;
			layout.safeChildren().add(element);
		} else if (parent instanceof BlockSection) {
			BlockSection section = (BlockSection)parent;
			section.safeChildren().add(element);
			element.setParent(section);
		} else {
			// TODO: show warning?
		}
	}
	
	private BlockSection getBlockSectionParent(Layout layout, Element parent, Integer newBlockSectionId) {
		BlockSection blockSection = null;
		if (parent != null) {
			blockSection = findElement(parent, BlockSection.class, true);
			if (blockSection != null) {
				return blockSection;
			}
		}
		if (newBlockSectionId == null) {
			return blockSection;
		}
		
		// create a new BlockSection automatically
		// here the root is Layout, see TagManager.createLayout
		
		List<Element> sections = layout.getChildren();
		for (Element section : sections) {
			if (section.getClass().equals(FlatSection.class)) {
				blockSection = (BlockSection) section;
				break;
			}
		}
		if (blockSection == null) {
			blockSection = layout.createFlatSection();
			blockSection.setId(newBlockSectionId);
		}
		return blockSection;
	}
	
	public Element createSectionElement(Layout layout, Element parent, Element newElement) {
		BlockSection blockSection = getBlockSectionParent(layout, parent, null);
		Element sectionElement;
		if (blockSection != null) {
			sectionElement = elementService.createSectionElement(blockSection, newElement);
			newElement = sectionElement;
		} else {
			if (parent == null) {
				int position = layout.safeChildren().size();
				newElement.setPositionRow(0);				// TODO: use parent.getColsPerRow()?
				newElement.setPositionCol(position);
				layout.safeChildren().add(newElement);
			} else {
				int position = parent.safeChildren().size();
				newElement.setPositionRow(0);				// TODO: use parent.getColsPerRow()?
				newElement.setPositionCol(position);
				parent.safeChildren().add(newElement);
				newElement.setParent(parent);
			}
		}
		return newElement;
	}

	public String createName(String name, String id) {
		// for the full name , please see Element.getNestedName(Element root, boolean useId)
		return name + id;
	}
	
	// NOTICE: it's not nested name
	public String createName(String tagName, Element parent, Layout root, TaglibManager tagManager, Model model) {
		int index = 1;
		String propertyName = tagName + index;
		
		String namePrefix = getNamePrefix(parent);
		
		// get the nested model name
		int pos = namePrefix == null ? 0 : namePrefix.indexOf('.');
		if (pos > 0) {
			String className = beanIntrospector.getModelName(model.getName());
			ModelPropertyInfo mpi = beanIntrospector.getModelPropertyInfo(className, namePrefix.substring(pos));
			if (mpi == null) {
				model = null;
			} else {
				model = modelService.getModel(mpi.modelInfo.getType());
			}
		}
		
		// create the new name
		// name doesn't exist in current element and model
		String newName = getFullName(namePrefix, propertyName);
		while (tagManager.findElement(root.getChildren(), newName) != null		// duplicated with current layout
				|| model != null && model.getModelField(propertyName) != null) {		// duplicated with the existed model
			index++;
			propertyName = tagName + index;
			newName = getFullName(namePrefix, propertyName);
		}
		return propertyName;
	}

	public String getName(Element element) {
		if (element.isContainer() || !isModelFieldTag(element)) {
			String name = element.getName();
			if (name != null && name.length() > 0) {
				if (name.startsWith("_gen_")) {
					return name;
				} else {
					if (element.getId() != null) {
						return "_gen_" + name + '_' + element.getId();
					} else {
						// it's OK even if it's not thread safe for 'nameIndex'
						return "_gen_" + name + '_' + (++nameIndex);
					}
				}
			} else {
				if (element.getId() != null) {
					return "_gen_" + guessNameByTagName(element) + '_' + element.getId();
				} else {
					return "_gen_" + guessNameByTagName(element) + '_' + (++nameIndex);
				}
			}
		} else {
			// TODO: check the SectionElement?
			return element.getName();
		}
	}
	
	public String createName(Layout root, String shortName) {
		// 1. populate the existed name
		Set<String> names = new HashSet<>(); 
		populateNames(root.getChildren(), names);
		
		// 2. choose one unique name
		if (shortName == null) {
			logger.warn("Found the null short name in layout : " + root.getName());
			shortName = "tag";
		}
		String prefix = "_gen_" + shortName + "_";
		int index = 0;
		String uniqueName = prefix + index;
		while (names.contains(uniqueName)) {
			uniqueName = prefix + (++index);
		}
		return  uniqueName;
	}
	
	private void populateNames(List<Element> elements, Set<String> names) {
		if (elements != null) {
			for (Element element : elements) {
				if (element instanceof BlockSection || element.getName() != null && element.getName().startsWith("_gen_")) {					
					names.add(element.getName());
					
					populateNames(element.getChildren(), names);
				}
			}
		}
	}
	
	public String getFullName(Element element) {
		if (element == null) {
			return null;
		}
		
		if (element.isContainer() || !isModelFieldTag(element)) {
			return getName(element);
		} else {
			if (element.getName() == null || element.getName().startsWith("_")) {
				return element.getName();
			} else {
				String namePrefix = getNamePrefix(element.getParent());
				String fullName = getFullName(namePrefix, element.getName());
				return fullName;
			}
		}
	}
	
	private String guessNameByTagName(Element element) {	
		// 1. from the extra property
		String tagName = element.getTagName();
				
		// 2. convert from the class
		if (tagName == null) {
			if (element instanceof AvabButton) {
				tagName = "button";
			} else if (element instanceof FlatSection){
				tagName = "container";
			} else if (element instanceof PageReference) {
				tagName = "include";
			}
			// MORE type
		}
		
		// 3. use class
		if (tagName == null) {
			String simpleName = StringUtils.uncapitalize(element.getClass().getSimpleName());
			if (simpleName.length() > 4) {
				tagName = simpleName.substring(0, 4);
			} else {
				tagName = simpleName;
			}
		}
		return '_' + tagName;
	}
	
	public String getFullName(String namePrefix, String shortName) {
		if (shortName != null && shortName.startsWith("_gen_")) {
			return shortName;
		}
		
		return namePrefix == null || namePrefix.length() == 0 ? shortName : namePrefix + '.' + shortName;
	}
	

	public String getFullName(String namePrefix, Element element) {
		return getFullName(namePrefix, element.getName());
	}
	
	/**
	 * 
	 * @param root
	 * @param parent 
	 * @return
	 */
	public String getNamePrefix(IElement parent) {
		if (parent instanceof Element) {
			Element element = (Element) parent;
			if (element.getParent() == null) {
				return element.getNamePrefix();
			} else {
				List<String> namePrefixs = new ArrayList<String>();
				do {
					if (element.getNamePrefix() != null) {
						namePrefixs.add(element.getNamePrefix());
					}
					element = element.getParent();
				} while (element != null);
				StringBuilder sb = new StringBuilder();
				for (int i = namePrefixs.size() - 1; i >= 0; i--) {
					String prefix = namePrefixs.get(i);
					// 'model' and 'execute' can only be the top prefix
					if ("model".equals(prefix) || "execute".equals(prefix)) {
						if (sb.length() > 0) {
							continue;
						}
					}
					
					if (sb.length() != 0) {
						sb.append('.');
					}
					sb.append(prefix);
				}
				return sb.toString();
			}
		} else {
			return null;
		}
	}
	
	public CssStyle getStyle(String style) {
		if (style == null || style.length() == 0) {
			return new CssStyle();
		}
		
		CssStyle cssStyle = new CssStyle();
		String[] propValues = StringUtils.split(style, ";");
		for (String propValue : propValues) {
			int index = propValue.indexOf(':');
			if (index > 0) {
				String key = propValue.substring(0, index).trim();
				String value = propValue.substring(index + 1).trim();
				cssStyle.add(key, value);
			}
		}
		return cssStyle;
	}
	
	public boolean isModelFieldTag(Element element) {
		String tagName = element.getTagName();
		return isModelFieldTag(tagName);
	}
	
	public boolean isModelFieldTag(String tagName) {
		if (tagManager.isModelFieldTag(tagName)) {
			return true;
		} else {
			return false;
		}
	}

	public String getNamePrefixNoModelPrefix(String namePrefix) {
		if (namePrefix.equals("model") || namePrefix.equals("execute")) {
			return "";
		}
		
		String namePrefixNoModelPrefix;
		if (namePrefix.startsWith("model.")) {
			namePrefixNoModelPrefix = namePrefix.substring(6);
		} else if (namePrefix.startsWith("execute.")) {
			namePrefixNoModelPrefix = namePrefix.substring(8);
		} else {
			namePrefixNoModelPrefix = namePrefix;
		}
		return namePrefixNoModelPrefix;
	}
	
	// TODO: add more tag?
	public String getRefFile(Element element) {
		String file = null;
		String tagName = element.getTagName();
		if ("css".equals(tagName) || "script".equals(tagName) || "image".equals(tagName)) {
			file = element.getExProperty("src");
		} else if ("include".equals(tagName) || "template".equals(tagName)) {
			file = element.getExProperty("url");
		} else if (element instanceof PageReference) {
			PageReference pageReference = (PageReference)element;
			file = pageReference.getUrl();
		}
		return file;
	}
	
	public void updateRefFile(Element element,String path) {
		String tagName = element.getTagName();
		if ("css".equals(tagName) || "script".equals(tagName) || "image".equals(tagName)) {
			element.addExProperty("src", path);
		} else if ("include".equals(tagName) || "template".equals(tagName)) {
			element.addExProperty("url", path);
		} else if (element instanceof PageReference) {
			PageReference pageReference = (PageReference)element;
			pageReference.setUrl(path);
		}
	}
	
	public static String getTagName(BeanIntrospector beanIntrospector, SectionElement sectionElement, ModelField modelField) {
		String tagName = sectionElement.getTagName();
		if (tagName != null || modelField == null) {
			return tagName;
		}
		
		if (modelField.getListType() != null && modelField.getListType()) {
			logger.debug("Wrong type? " + modelField.getName());
		}
		
		String genericType = modelField.getGenericType();
		Class clazz = null;
		if (genericType != null) {
			clazz = ClassUtil.forName(genericType);
		}
		if (clazz != null && clazz.isEnum()) {
			tagName = "enumselect";
		} else if (beanIntrospector.isModel(genericType) >= 0) {
			// all the domain model is modelselect
			if (genericType.equals("com.espirit.eap.manager.I18n")) {				
				tagName = "i18n";
			} else {
				tagName = "modelselect";
			}
		} else if (genericType == null || BaseTypeModel.DEFMODEL_STRING.equals(genericType)) {
			if (sectionElement.getExProperty("textArea") != null) {
				tagName = "textarea";
			} else {
				tagName = "text";
			}
		} else if (BaseTypeModel.DEFMODEL_LIST.equals(genericType)) {
			if (sectionElement.getExProperty("checkBoxList") != null) {
				tagName = "checkboxlist";
			} else {
				tagName = "modelselect";
			}
		} else if (BaseTypeModel.DEFMODEL_DATE.equals(genericType)) {
			tagName = "date";
		} else if (BaseTypeModel.DEFMODEL_INTEGER.equals(genericType)) {
			tagName = "number";
		} else if (BaseTypeModel.DEFMODEL_DECIMAL.equals(genericType)) {
			tagName = "number";
		} else if (BaseTypeModel.DEFMODEL_BOOLEAN.equals(genericType)) {
			tagName = "checkbox";
		} else if (BaseTypeModel.DEFMODEL_FILE.equals(genericType)) {
			tagName = "file";
		} else {
			tagName = "text";
		}
		return tagName;
	}
	

	public String createTemplatePath(Layout root, String modelPath) {
		int fileNameIndex = modelPath.lastIndexOf('.') + 1;
		String fileExtension = ".ftl";
		final Set<String> files = new HashSet<>();
		elementService.iterator(root.getChildren(), new ElementCallback() {
			@Override
			public boolean process(Element parent, Element element, int deep) {
				String refFile = getRefFile(element);
				if (refFile != null) {
					files.add(refFile);
				}
				return false;
			}
		}, 0);
		int fileIndex = 0;
		String url;
		do {
			url = config.getViewDir4GenCode() + modelPath.substring(1, fileNameIndex) + fileIndex + fileExtension;
			fileIndex++;
		} while(files.contains(url));
		return url;
	}
}
